15. 빈 스코프
15. 빈 스코프
빈 스코프
빈 스코프란?
지금까지 스프링 컨테이너 라이프 사이클 > 빈 라이프 사이클 포함 관계였다.
하지만 스프링은 다양한 스코프 또한 지원한다. (스코프란 생명 수명이다.)
스프링이 지원하는 빈 스코프
- 싱글톤 : 기본 스코프, 스프링 컨테이너와 함께한다.
- 프로토 타입 : 스프링 컨테이너가 생성과 의존관계 주입까지만 관여한다. 추후에는 관리하지 않는다.
웹 관련 스코프
- request : 하나의 요청에 관해서 들어오고 나올떄까지의 스코프
- session : 웹 세션이 생성되고 종료 될 때까지의 스코프
- application : 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프
프로토타입 스코프
- 기본 스코프가 아니다.
- 즉 싱글톤이 아니다
- 즉 객체만 생성하고 전혀 관리하지 않는다.
- 핵심 : 즉 생성에만 관리한다. → 즉 추후 빈 관리는 클라이언트에게 있다.
@PreDestroy
같은 메서드가 실행되지 않는다.- 종료처리를 스프링이 해주지 않고 클라이언트가 직접 해야한다.
- 즉 각 bean을 두번 실행하면 다른 bean 두개가 실행된다.
프로토 타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
- 스프링에서는 프로토타입시에 항상 새로운 객체를 인스턴스로 생성해서 반환한다.
- 만약에 싱글톤 빈과 함께 사용한다면?
- 즉 싱글톤 빈에서 의존관계 주입으로 프로토타입 빈을 받는 것
- 싱글톤 빈이 필요하다면 스프링은 의존관계 주입을 한다.
- 프로토타입 빈도 주입을 해준다.
- 싱글톤빈은 프로토타입 빈을 내부 필드에 관리한다.
- 즉 더이상 프로토타입 빈은 스프링 내부에서 관리하지 않는다. 4. 추후 누군가가 싱글톤빈을 호출하면 같은 싱글톤빈이 호출되어진다. 5. 그러면 프로토타입 빈은 싱글톤 빈 내부에 있기에 여전히 같은 객체가 사용자에게 전달된다…
→ 프로토타입의 성질을 잃음
- 즉 싱글톤 빈에서 의존관계 주입으로 프로토타입 빈을 받는 것
이러한 부수효과를 언제나 프로그래밍에서 조심할것…
Provider로 문제 해결
- 의존관계를 외부에서 주입 받는게 아니라 의존관계를 찾는 것을 dl이라한다.
- 스프링에는 dl을 해주는 것이 준비되어 있다.
ObjcectFactory, ObjectProvider
- Provider가 자식이다. → 기능 확장
- 본질적으로는 같다.
prototypeBeanProvider.getObject()
을 통해 항상 새로운 타입의 프로토타입 빈을 제공한다.- 즉 스프링 컨테이너가 해당 빈을 찾아서 반환한다.
- 별도의 라이브러리는 필요 없지만 스프링에 의존한다.
1
2
3
4
5
6
private final ObjectProvider<MyLogger> myLoggerProvider;
public String logDemo(HttpServletRequest request) throws Exception {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
)
javax.inject:javax.inject:1을 gradle에 추가시 DL 스프링을 사용하지 않고도 기능을 사용할 수 있다. 하지만… 대부분의 이런 기능이나 다른 라이브러리와 비슷한 기능을 제공할 경우 스프링 컨테이너에서 사용하는 기능으로 통일하는 것이 좋다.
왜 프로토타입 Bean을 사용할까?
- 매번 필요한 빈을 생성해야할 때
- 하지만 대부분 상황에서는 Bean을 싱글톤으로 사용할 일이 더 많다.
- 즉… 대부분의 경우는 싱글톤을 사용하고 필요하면
new
메서드를 사용해 프로토타입을 제어해도 충분하다.
웹 스코프
- 웹 환경에서만 동작한다.
- 스프링이 프로토타입과 달리 종료시점까지는 관리 해준다. → 종료 메서드 호출
웹 스코프의 종류
- request
- HTTP 요청 하나가 들어오고 나갈 떄까지 유지되는 스코프, 각 HTTP 요청보다 별도의 bean 이 생성된다.
- session
- HTTP Session과 동일한 생명주기를 가지는 스코프
- application
servletcontext
와 동일한 생명주기를 가지는 스코프
- websocket
- 웹 소켓과 동일한 생명 주기를 가지는 스코프
request 스코프 실습
gradle 설정
1
implementation 'org.springframework.boot:spring-boot-starter-web'
- 이걸 하면 스프링 부트는 내장 톰캣 서버를 통해 웹 서버를 실행한다.
로그를 사용한 예제 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public MyLogger() {
}
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "] [" + requestURL + "] [" + message + "]");
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] 초기화 " + this );
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] 종료 " + this );
}
}
- 로그를 출력하기 위한
myLogger
클래스 - 이 빈은 HTTP 요청당 하나씩 생성된다.
- 절대 중복되지 않는 uuid를 사용한다.
requestURL
은 이 빈이 생성되는 시점에는 알 수 없기에 setter를 사용한다.
컨트롤러
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
public class LogDemoController {
private final LogDemoController logDemoController;
private final MyLogger myLogger;
public LogDemoController(LogDemoController logDemoController, MyLogger myLogger) {
this.logDemoController = logDemoController;
this.myLogger = myLogger;
}
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controllerrrr");
return "ok";
}
}
- 하지만 이코드는 오류가 뜬다. 왜?
- Spring 컨테이너가 Bean 객체를 만들려고 하는데 request 요청이 없기에 객체를 생성할 수가 없다.
Provider를 사용한 해결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
public class LogDemoController {
private final LogDemoController logDemoController;
private final ObjectProvider<MyLogger> myLoggerObjectProvider;
public LogDemoController(LogDemoController logDemoController, ObjectProvider<MyLogger> myLoggerObjectProvider) {
this.logDemoController = logDemoController;
this.myLoggerObjectProvider = myLoggerObjectProvider;
}
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerObjectProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controllerrrr");
return "ok";
}
}
- provider를 통해 적절한 시기에 Bean을 주입받을 수 있다!
.getObjcet()
를 사용하기에 service, controller 등 어떠한 곳에서도 같은 HTTP 요청임을 구분할 수 있다!
코드를 더 줄이는 방법 - Proxy
1
2
3
4
5
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
...
}
proxyMode = ScopedProxyMode.TARGET_CLASS
- 적용 대상이 인터페이스면
INTERFACES
를 선택 - 클래스라면 위 처럼 적는다.
- 적용 대상이 인터페이스면
- 이런 식으로 MyLogger의 가짜 프록시 클래스(CGLIB)를 만들어 놓고 HTTP request와 상관 없이 가짜 프록시 빈을 주입시켜놓는다!
- 스프링 컨테이너가 CGLIB라는 바이트 코드 조작 라이브러리를 사용한다.
- 의존관계에 미리 주입시켜놓기에
getBean()
에서도 프록시 객체가 조회된다.
가짜 프록시 객체는 요청이 오면 그때 진짜 Bean을 요청하는 위임 로직이 들어가있다.
- 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기에 클라이언트 입장에선 동일하게 사용가능 → 다형성
- 마찬가지로 싱글톤처럼 동작한다.
특징 정리
- 이거 덕분에 편리하게 request scope을 사용할 수 있다.
- Provider나 프록시를 사용하면 쉽게 지연 처리를 제공한다.
- 어노테이션 설정 변경만으로 프록시 객체를 대체 가능 → 다형성과 DI컨테이너의 큰 강점
- 웹 스코프 뿐만 아니라 다른 곳에서 응용 가능
주의점 : 싱글톤처럼 보이지만 아니다. 즉 이런 scope는 주의해서 사용한다. 유지보수에 어려움 을 겪을 수 있다…
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.